[AWS CDK] Lambda関数からRDS Proxy経由でAmazon Aurora DBクラスターに接続してみた
なんとなくLambda関数からRDS Proxy経由でAmazon Aurora DBクラスターに接続したいな
こんにちは、のんピ(@non____97)です。
皆さんはLambda関数からRDS Proxy経由でAmazon Aurora DBクラスターに接続したいと思ったことはありますか? 私はあります。
今までLambda関数からRDBに接続したことがなかったので、なんとなくやってみたいと思いました。
また、最近まだまだ寒いです(2022/4/4)。マネージメントコンソールから真心を込めて作るのも冷え性の私にとっては非常に辛いです。そこでAWS CDKで全てのリソースを作成します。
こちらのコードのリポジトリは以下になります。
作成されるリソースの構成
作成されるリソースの構成は以下の通りです。
DBクラスター接続用Lambda関数からRDS Proxyを経由してAmazon Aurora DBクラスター(PostgreSQL 13.4)に接続できることを確認します。
Amazon Linux 2のEC2インスタンスはDB内にテーブルを作成したりなどの初期設定で使用します。
また、DBクラスター接続用Lambda関数だけでなくRDS Proxy自身もSecrets Managerにシークレットを取得しに行く必要があるので、RDS ProxyもNAT Gatewayへのルーティングがあるサブネットに配置しています。もし、インターネットにルーティングされることが気になるようであれば、Secrets ManagerのVPCエンドポイントを作成し、RDS ProxyをAmazon Aurora DBクラスターと同じサブネットに配置します。
つまづいたポイント
つまづいたポイントは以下の2点です。
- pg-nativeの取り扱い
- TLSを使ったDBクラスターへの接続
まず、1つ目のpg-nativeの取り扱いについてです。
Lambda関数からDBクラスターに接続するにあたって、PostgreSQLのクライアントをインストールする必要があります。
今回はnode-postgresを使用しました。
こちらを使用してnpx cdk deploy
をしたところ、以下のようにエラーが出力されました。
> npx cdk deploy MFA token for arn:aws:iam::<AWSアカウントID>:mfa/<IAMユーザー名>: 430772 Bundling asset LambdaAuroraStack/DbAccessFunction/Code/Stage... ✘ [ERROR] Could not resolve "pg-native" node_modules/pg/lib/native/client.js:4:21: 4 │ var Native = require('pg-native') ╵ ~~~~~~~~~~~ You can mark the path "pg-native" as external to exclude it from the bundle, which will remove this error. You can also surround this "require" call with a try/catch block to handle this failure at run-time instead of bundle-time. 1 error /<ディレクトリパス>a/node_modules/aws-cdk-lib/core/lib/asset-staging.ts:474 throw new Error(`Failed to bundle asset ${this.node.path}, bundle output is located at ${bundleErrorDir}: ${err}`); ^ Error: Failed to bundle asset LambdaAuroraStack/DbAccessFunction/Code/Stage, bundle output is located at /<ディレクトリパス>a/cdk.out/bundling-temp-26fc234568dafead65a0b40a064d644e0d951743e4c3676911b851738b116dc6-error: Error: bash -c npx --no-install esbuild --bundle "/<ディレクトリパス>a/src/lambda/handlers/db-access.ts" --target=node14 --platform=node --outfile="/<ディレクトリパス>a/cdk.out/bundling-temp-26fc234568dafead65a0b40a064d644e0d951743e4c3676911b851738b116dc6/index.js" --minify --sourcemap --external:aws-sdk run in directory /<ディレクトリパス>a exited with status 1 at AssetStaging.bundle (/<ディレクトリパス>a/node_modules/aws-cdk-lib/core/lib/asset-staging.ts:474:13) at AssetStaging.stageByBundling (/<ディレクトリパス>a/node_modules/aws-cdk-lib/core/lib/asset-staging.ts:322:10) at stageThisAsset (/<ディレクトリパス>a/node_modules/aws-cdk-lib/core/lib/asset-staging.ts:188:35) at Cache.obtain (/<ディレクトリパス>a/node_modules/aws-cdk-lib/core/lib/private/cache.ts:24:13) at new AssetStaging (/<ディレクトリパス>a/node_modules/aws-cdk-lib/core/lib/asset-staging.ts:213:44) at new Asset (/<ディレクトリパス>a/node_modules/aws-cdk-lib/aws-s3-assets/lib/asset.ts:131:21) at AssetCode.bind (/<ディレクトリパス>a/node_modules/aws-cdk-lib/aws-lambda/lib/code.ts:282:20) at new Function (/<ディレクトリパス>a/node_modules/aws-cdk-lib/aws-lambda/lib/function.ts:692:29) at new NodejsFunction (/<ディレクトリパス>a/node_modules/aws-cdk-lib/aws-lambda-nodejs/lib/function.ts:100:5) at new LambdaAuroraStack (/<ディレクトリパス>a/lib/lambda-aurora-stack.ts:289:5) Subprocess exited with error 1
node-postgresのIssueを探したところ、同様のチケットがありました。
このチケットではBabelやwebpackでバンドルした際にこのエラーが発生した場合の回避方法が紹介されています。IgnorePlugin
で回避しているというコメントがあったので、同様にLambda関数をビルドする際にpg-nativeはバンドルしないようにします。
実際のコードは以下の通りです。
// Lambda Function DB access new nodejs.NodejsFunction(this, "DbAccessFunction", { entry: "./src/lambda/handlers/db-access.ts", runtime: lambda.Runtime.NODEJS_14_X, bundling: { minify: true, sourceMap: true, externalModules: ["pg-native"],
2つ目はTLSを使ったDBクラスターへの接続です。
Lambda関数はDBクライアントを定義する際にssl: true
とすることで、TLSでDBに接続できます。
// DB Client const dbClient = new Client({ user: secret.username, host: process.env.PGHOST, database: secret.dbname, password: secret.password, port: secret.port, ssl: true, });
ただ、証明書を指定して接続したいときもありますよね?
ということで、証明書を指定して接続してみます。
まず、証明書を用意します。
証明書は以下AWS公式ドキュメントで紹介されている通り、https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem からダウンロードします。
ダウンロードしたファイルは./src/cert/global-bundle.pem
に保存しました。
次にこちらの証明書をLambda関数にアップロードします。
その際はaws-cdk-lib.aws_lambda_nodejs moduleのCommand hooksを使用します。
実際のコードは以下の通りです。
// Lambda Function DB access new nodejs.NodejsFunction(this, "DbAccessFunction", { entry: "./src/lambda/handlers/db-access.ts", runtime: lambda.Runtime.NODEJS_14_X, bundling: { minify: true, sourceMap: true, externalModules: ["pg-native"], commandHooks: { beforeBundling() { return []; }, afterBundling(inputDir: string, outputDir: string): string[] { return [`cp -p ./src/cert/global-bundle.pem ${outputDir}`]; }, beforeInstall() { return []; }, }, },
最後にアップロードした証明書をLambda関数内で読み込みます。
// DB Client const dbClient = new Client({ user: secret.username, host: process.env.PGHOST, database: secret.dbname, password: secret.password, port: secret.port, ssl: { rejectUnauthorized: true, cert: fs.readFileSync("global-bundle.pem", "utf-8").toString(), }, });
ちなみにRDS Proxy側でTLSを強制している状態でTLSを使用しないで接続しようとすると、以下のようにエラーが出力されて接続することができません。
{ "errorType": "error", "errorMessage": "This RDS Proxy requires TLS connections", "code": "28000", "length": 67, "name": "error", "severity": "FATAL", "stack": [ "error: This RDS Proxy requires TLS connections", " at Em.parseErrorMessage (/node_modules/pg-protocol/src/parser.ts:369:69)", " at Em.handlePacket (/node_modules/pg-protocol/src/parser.ts:188:21)", " at Em.parse (/node_modules/pg-protocol/src/parser.ts:103:30)", " at Socket.<anonymous> (/node_modules/pg-protocol/src/index.ts:7:48)", " at Socket.emit (events.js:400:28)", " at addChunk (internal/streams/readable.js:293:12)", " at readableAddChunk (internal/streams/readable.js:267:9)", " at Socket.Readable.push (internal/streams/readable.js:206:10)", " at TCP.onStreamRead (internal/stream_base_commons.js:188:23)" ] }
DBクラスター接続用のLambda関数の処理の説明
DBクラスター接続用のLambda関数で行っている処理は以下の通りです。
- Secrets ManagerからDBクラスターのシークレットを取得
- 取得したシークレットを利用してRDS Proxy経由でDBクラスターに接続
test_table
テーブルのレコードを表示test_table
テーブルにレコードを追加- 再度
test_table
テーブルのレコードを表示 - DBクラスターとの接続のクローズ
実際のコードは以下の通りです。
import { SecretsManagerClient, GetSecretValueCommand, } from "@aws-sdk/client-secrets-manager"; import { Client } from "pg"; import * as fs from "fs"; export const handler = async (): Promise<void | Error> => { // Get secret value const secretsManagerClient = new SecretsManagerClient({ region: process.env.AWS_REGION!, }); const getSecretValueCommand = new GetSecretValueCommand({ SecretId: process.env.SECRET_ID, }); const getSecretValueCommandResponse = await secretsManagerClient.send( getSecretValueCommand ); const secret = JSON.parse(getSecretValueCommandResponse.SecretString!); // DB Client const dbClient = new Client({ user: secret.username, host: process.env.PGHOST, database: secret.dbname, password: secret.password, port: secret.port, ssl: { rejectUnauthorized: false, cert: fs.readFileSync("global-bundle.pem").toString(), }, // Also OK // ssl: true, }); // DB Connect await dbClient.connect(); // Query const beforeInsertQuery = await dbClient.query("SELECT * FROM test_table"); console.log(beforeInsertQuery.rows); const insertQuery = await dbClient.query( "INSERT INTO test_table (name) VALUES ($1)", ["non-97"] ); console.log(insertQuery.rows); const afterInsertQuery = await dbClient.query("SELECT * FROM test_table"); console.log(afterInsertQuery.rows); // DB Connect Close await dbClient.end(); return; };
動作確認
それでは、npx cdk deploy
で各種リソースをデプロイします。
npx cdk deploy
実行後にマネージメントコンソールを開くと、Aurora DBクラスターやRDS Proxy、DBクラスター接続用のLambda関数など各種リソースが正常に作成できていることを確認できました。
- Aurora DBクラスター
-
RDS Proxy
-
DBクラスター接続用のLambda関数
それでは、EC2インスタンスからAurora DBクラスターに接続します。
$ get_secrets_value=$(aws secretsmanager get-secret-value \ --secret-id prd-db-cluster/AdminLoginInfo \ --region us-east-1 \ | jq -r .SecretString) $ export PGHOST=db-proxy.proxy-cicjym7lykmq.us-east-1.rds.amazonaws.com $ export PGPORT=$(echo "${get_secrets_value}" | jq -r .port) $ export PGDATABASE=$(echo "${get_secrets_value}" | jq -r .dbname) $ export PGUSER=$(echo "${get_secrets_value}" | jq -r .username) $ export PGPASSWORD=$(echo "${get_secrets_value}" | jq -r .password) $ psql psql (13.6, server 13.4) SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off) Type "help" for help. testDB=> testDB=> \conninfo You are connected to database "testDB" as user "postgresAdmin" on host "db-proxy.proxy-cicjym7lykmq.us-east-1.rds.amazonaws.com" (address "10.10.0.46") at port "5432". SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
次にテスト用のテーブルを作成します。
testDB=> CREATE TABLE test_table ( id SERIAL NOT NULL, name VARCHAR(255) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ); CREATE TABLE testDB=> testDB=> \dt List of relations Schema | Name | Type | Owner --------+------------+-------+--------------- public | test_table | table | postgresAdmin (1 row) testDB=> testDB=> \d test_table Table "public.test_table" Column | Type | Collation | Nullable | Default ------------+-----------------------------+-----------+----------+---------------------------------------- id | integer | | not null | nextval('test_table_id_seq'::regclass) name | character varying(255) | | not null | created_at | timestamp without time zone | | not null | CURRENT_TIMESTAMP Indexes: "test_table_pkey" PRIMARY KEY, btree (id) testDB=>
作成したテーブルにレコードを追加します。
testDB=> INSERT INTO test_table (name) VALUES ('non-97'); INSERT 0 1 testDB=> testDB=> SELECT * FROM test_table; id | name | created_at ----+--------+---------------------------- 1 | non-97 | 2022-04-04 17:41:21.988478 (1 row) testDB=> INSERT INTO test_table (name) VALUES ('non-97'); INSERT 0 1 testDB=> SELECT * FROM test_table; id | name | created_at ----+--------+---------------------------- 1 | non-97 | 2022-04-04 17:41:21.988478 2 | non-97 | 2022-04-04 17:42:18.329333 (2 rows)
2件のレコードが追加されたことを確認できました。
それでは、DBクラスター接続用のLambda関数を実行します。
DBクラスター接続用のLambda関数を実行すると以下のようなログが出力されました。
START RequestId: 89c47b87-81fb-41a4-aaae-e241d3285403 Version: $LATEST 2022-04-04T09:07:54.409Z 89c47b87-81fb-41a4-aaae-e241d3285403 INFO [ { id: 1, name: 'non-97', created_at: 2022-04-04T17:41:21.988Z }, { id: 2, name: 'non-97', created_at: 2022-04-04T17:42:18.329Z } ] 2022-04-04T09:07:54.466Z 89c47b87-81fb-41a4-aaae-e241d3285403 INFO [] 2022-04-04T09:07:54.486Z 89c47b87-81fb-41a4-aaae-e241d3285403 INFO [ { id: 1, name: 'non-97', created_at: 2022-04-04T17:41:21.988Z }, { id: 2, name: 'non-97', created_at: 2022-04-04T17:42:18.329Z }, { id: 3, name: 'non-97', created_at: 2022-04-04T18:07:54.449Z } ] END RequestId: 89c47b87-81fb-41a4-aaae-e241d3285403 REPORT RequestId: 89c47b87-81fb-41a4-aaae-e241d3285403 Duration: 1000.24 ms Billed Duration: 1001 ms Memory Size: 128 MB Max Memory Used: 69 MB Init Duration: 288.08 ms XRAY TraceId: 1-624ab568-318e78e3790e197c5b99ca95 SegmentId: 5ef91179409d5a94 Sampled: true
正しくレコードの表示、追加がされていますね。
念の為もう一度実行しましたが、以下のように正しくレコードの表示、追加が出来ています。
START RequestId: bf75845a-fc3d-41a8-b1e4-e0369833f14d Version: $LATEST 2022-04-04T11:06:47.487Z bf75845a-fc3d-41a8-b1e4-e0369833f14d INFO [ { id: 1, name: 'non-97', created_at: 2022-04-04T17:41:21.988Z }, { id: 2, name: 'non-97', created_at: 2022-04-04T17:42:18.329Z }, { id: 3, name: 'non-97', created_at: 2022-04-04T18:07:54.449Z } ] 2022-04-04T11:06:47.545Z bf75845a-fc3d-41a8-b1e4-e0369833f14d INFO [] 2022-04-04T11:06:47.565Z bf75845a-fc3d-41a8-b1e4-e0369833f14d INFO [ { id: 1, name: 'non-97', created_at: 2022-04-04T17:41:21.988Z }, { id: 2, name: 'non-97', created_at: 2022-04-04T17:42:18.329Z }, { id: 3, name: 'non-97', created_at: 2022-04-04T18:07:54.449Z }, { id: 4, name: 'non-97', created_at: 2022-04-04T20:06:47.527Z } ] END RequestId: bf75845a-fc3d-41a8-b1e4-e0369833f14d REPORT RequestId: bf75845a-fc3d-41a8-b1e4-e0369833f14d Duration: 937.91 ms Billed Duration: 938 ms Memory Size: 128 MB Max Memory Used: 69 MB Init Duration: 244.94 ms XRAY TraceId: 1-624ad146-74b55a8e75775490061291a7 SegmentId: 1f11c84f47a696a9 Sampled: true
また、その際のRDS Proxyのログを確認すると以下のようになっていました。
2022-04-04T09:07:54.176Z [INFO] [proxyEndpoint=default] [clientConnection=339077222] A new client connected from 10.10.0.38:37297. 2022-04-04T09:07:54.315Z [DEBUG] [proxyEndpoint=default] [clientConnection=339077222] Received Startup Message: [username="postgresAdmin", database="testDB", protocolMajorVersion=3, protocolMinorVersion=0, sslEnabled=true] 2022-04-04T09:07:54.328Z [DEBUG] [proxyEndpoint=default] [clientConnection=339077222] Proxy authentication with PostgreSQL native password authentication succeeded for user "postgresAdmin" with TLS on. 2022-04-04T09:07:54.349Z [INFO] [dbConnection=2685642450] A TCP connection was established from the proxy at 10.10.0.53:1319 to the database at 10.10.0.86:5432. 2022-04-04T09:07:54.391Z [DEBUG] [dbConnection=2685642450] The new database connection successfully authenticated with TLS on. 2022-04-04T09:07:54.395Z [DEBUG] [proxyEndpoint=default] [clientConnection=339077222] The client connection borrowed the database connection [dbConnection=2685642450] for the next query/transaction. 2022-04-04T09:07:54.398Z [DEBUG] [proxyEndpoint=default] [clientConnection=339077222] The database connection [dbConnection=2685642450] borrowed from the connection pool is being released to the connection pool. 2022-04-04T09:07:54.449Z [DEBUG] [proxyEndpoint=default] [clientConnection=339077222] The client connection borrowed the database connection [dbConnection=2685642450] for the next query/transaction. 2022-04-04T09:07:54.449Z [WARN] [proxyEndpoint=default] [clientConnection=339077222] The client session was pinned to the database connection [dbConnection=2685642450] for the remainder of the session. The proxy can't reuse this connection until the session ends. Reason: A parse message was detected. 2022-04-04T09:07:54.488Z [INFO] [proxyEndpoint=default] [clientConnection=339077222] The client connection closed. Reason: The client requested that the connection close. 2022-04-04T09:07:54.489Z [DEBUG] [proxyEndpoint=default] [clientConnection=339077222] The database connection [dbConnection=2685642450] borrowed from the connection pool is being released to the connection pool.
2022-04-04T11:06:47.273Z [INFO] [proxyEndpoint=default] [clientConnection=2790996939] A new client connected from 10.10.0.61:13632. 2022-04-04T11:06:47.412Z [DEBUG] [proxyEndpoint=default] [clientConnection=2790996939] Received Startup Message: [username="postgresAdmin", database="testDB", protocolMajorVersion=3, protocolMinorVersion=0, sslEnabled=true] 2022-04-04T11:06:47.430Z [DEBUG] [proxyEndpoint=default] [clientConnection=2790996939] Proxy authentication with PostgreSQL native password authentication succeeded for user "postgresAdmin" with TLS on. 2022-04-04T11:06:47.450Z [DEBUG] [proxyEndpoint=default] [clientConnection=2790996939] The client connection borrowed the database connection [dbConnection=3594132134] for the next query/transaction. 2022-04-04T11:06:47.456Z [DEBUG] [proxyEndpoint=default] [clientConnection=2790996939] The database connection [dbConnection=3594132134] borrowed from the connection pool is being released to the connection pool. 2022-04-04T11:06:47.526Z [DEBUG] [proxyEndpoint=default] [clientConnection=2790996939] The client connection borrowed the database connection [dbConnection=3594132134] for the next query/transaction. 2022-04-04T11:06:47.526Z [WARN] [proxyEndpoint=default] [clientConnection=2790996939] The client session was pinned to the database connection [dbConnection=3594132134] for the remainder of the session. The proxy can't reuse this connection until the session ends. Reason: A parse message was detected. 2022-04-04T11:06:47.567Z [INFO] [proxyEndpoint=default] [clientConnection=2790996939] The client connection closed. Reason: The client requested that the connection close. 2022-04-04T11:06:47.569Z [DEBUG] [proxyEndpoint=default] [clientConnection=2790996939] The database connection [dbConnection=3594132134] borrowed from the connection pool is being released to the connection pool.
Lambda関数からRDS Proxy経由でAmazon Aurora DBクラスターに接続するのは思ったより簡単
Lambda関数からRDS Proxy経由でAmazon Aurora DBクラスターに接続してみました。
単純にDBクラスターに接続する程度であれば非常に簡単ですね。
この記事が誰かの助けになれば幸いです。
以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!